<?php

/**
 * Kaeru Framework
 *
 * LICENSE:
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * The license is bundled with this package in the file LICENSE.txt.
 * You may also obtain a copy of the License at this URL:
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @copyright  Copyright (c) 2009, Jonathan Hedrén
 * @author	   Jonathan Hedrén <jonathan.hedren@gmail.com>
 * @license    http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
 */
 
namespace Kaeru\Model\Dao;

use \Kaeru\Core\Utility;
use \Kaeru\Model;
use \Kaeru\Model\AbstractModel;
use \Kaeru\Storage\Db\IConstants;
use \Kaeru\Storage\Db\Query\SelectQuery;

/**
 * @see Kaeru_Dao_Abstract
 */
require_once 'Kaeru/Model/Dao/AbstractDao.php';

/**
 * @abstract
 */
abstract class AbstractDbDao extends AbstractDao
{
	
    /**
     * @var mixed
     */
    protected $currentModelId;

    /**
     * @var array
     */
    protected $fetchPlan;

    /**
     * @todo maybe move this to \Kaeru\Dao\AbstractDao
     *
     * @var array
     */
    protected $modelCache = array();

    /**
     * The name of the column to use as primary key.
     *
     * @var string
     */
    protected $primaryKey = 'id';

    /**
     * This array is used to determine which cache keys to use for deleting and
     * updating any cache when a model is deleted or saved.
     *
     * @var array
     */
    protected $cacheKeyMapping = array();

    /**
     * Maps one or more model properties to a table column. By default the
     * model's property name will be used as table column name. This array
     * allows for different column names vs property names.
     *
     * Example:
     * 'property' => 'column'
     *
     * @var array
     */
    protected $tableColumnMapping = array();

    /**
     * Specify any table joins for the model.
     *
     * IMPORTANT: By default no data will be saved to the joined tables when
     * calling Kaeru_Dao_Abstract_Db::save().
     *
     * Example:
     * 'table2' => array(
     *		'type' => 'INNER JOIN',
     *		'on' => 'table1.column = table2.column',
     * )
     *
     * @var array
     */
    protected $tableJoins = array();

    /**
     * The name of the database table to fetch/save data from/to.
     *
     * @var string
     */
    protected $tableName;


    /**
     * Applies table joins if available.
     *
     * @param \Kaeru\Storage\Db\Query\SelectQuery $select
     * @return \Kaeru\Model\Dao\AbstractDbDao
     */
    protected function applyTableJoins(SelectQuery $select)
    {
        if (!empty($this->tableJoins)) {
            foreach ($this->tableJoins as $tableName => $joinOptions) {
                if ($joinOptions['type'] == IConstants::SQL_LEFT_JOIN) {
                    $select->joinLeft(
                        $tableName,
                        $joinOptions['on'],
                        $this->getFetchPlanForTable($tableName)
                    );
                }
                else if ($joinOptions['type'] == IConstants::SQL_INNER_JOIN) {
                    $select->joinInner(
                            $tableName,
                            $joinOptions['on'],
                            $this->getFetchPlanForTable($tableName)
                    );
                }
            }
        }

        return $this;
    }

    /**
     * Builds the query to fetch a row from the database.
     *
     * @return \Kaeru\Storage\Db\Query\SelectQuery
     */
    protected function buildGetCollectionQuery($params)
    {
        if ($this->tableName == null) {
            throw new \Exception('$tableName must be set.');
        }

        $select = $this->storage->select();
        $select->from($this->tableName, $this->getFetchPlanForTable($this->tableName));

        $this->applyTableJoins($select);

        if (isset($params['orderBy']) && isset($params['orderDir'])) {
            $select->order($params['orderBy'], $params['orderDir']);
        }

        if (isset($params['limit']) && isset($params['offset'])) {
            $select->limit($params['limit'], $params['offset']);
        }

        return $select;
    }

    /**
     * Builds the query to fetch a row from the database.
     *
     * @return \Kaeru\Storage\Db\Query\SelectQuery
     */
    protected function buildGetQuery()
    {
        if ($this->tableName == null) {
            throw new \Exception('$tableName must be set.');
        }
        $select = $this->storage->select();
        $select->from($this->tableName, $this->getFetchPlanForTable($this->tableName));

        if ($this->currentModelId !== null) {
            $select->where($this->tableName . '.' . $this->primaryKey . ' =', $this->currentModelId);
        }
        
        $select->limit(1);

        $this->applyTableJoins($select);

        return $select;
    }

    /**
     *
     * @param mixed
     * @return string
     */
    protected function createCacheKey($sql, array $ids = array())
    {
        return $this->modelClass . '-' . md5($sql . implode('', $ids));
    }

    /**
     * Deletes a model. Returns true on success, else false.
     *
     * @param \Kaeru\Model\AbstractModel $model
     * @return boolean
     */
    public function delete(AbstractModel $model)
    {
        $primaryKey = $this->primaryKey;
        if (!$model->$primaryKey) {
            return false;
        }

        $result = $this->storage->delete($this->tableName, $primaryKey . ' =' . $model->$primaryKey);
        if ($result) {
            $this->deleteCache($model);
            unset($model);
            // return $result->getAffectedRows()
            return true;
        }
        return false;
    }

    /**
     * Deletes any cache of the model.
     *
     * @param \Kaeru\Model\AbstractModel $model
     * @return void
     */
    public function deleteCache($model)
    {
        $primaryKey = $this->primaryKey;
        unset($this->modelCache[$model->$primaryKey]);
        if ($this->cacheStorage !== null) {
            if (!isset($this->cacheKeyMapping[$model->$primaryKey])) {
                return;
            }
            foreach ($this->cacheKeyMapping[$model->$primaryKey] as $cacheKey) {
                $this->cacheStorage->clean($cacheKey);
            }
        }
    }

    /**
     * Gets a single model.
     *
     * @param mixed $id
     * @param mixed $params
     * @return \Kaeru\Model\AbstractModel
     */
    public function get($id)
    {
        if ($id === null) {
            return null;
        }
        $this->currentModelId = $id;
        return $this->queryAndCreateModel($this->buildGetQuery());
    }

    /**
     * Gets a collection of models.
     *
     * @param $params
     * @return array
     */
    public function getCollection($params = array())
    {
        return $this->queryAndCreateModels($this->buildGetCollectionQuery($params));
    }

    /**
     *
     *
     * @return array
     * @param string $table
     */
    protected function getFetchPlanForTable($table)
    {
        if ($this->fetchPlan === null || !isset($this->fetchPlan[$table])) {
            return '*';
        }
        return $this->fetchPlan[$table];
    }

    /**
     * Maps a cache key to a primary key (most usually the id of the model).
     *
     * @param mixed $cacheKey
     * @param mixed $primaryKey
     */
    protected function mapCacheKey($cacheKey, $primaryKey)
    {
        if (!isset($this->cacheKeyMapping[$primaryKey])) {
            $this->cacheKeyMapping[$primaryKey] = array();
        }
        $this->cacheKeyMapping[$primaryKey][] = $cacheKey;
    }

    /**
     * Queries the database, initializes a model and sets
     * it's attribute values from the result (if any).
     *
     * @param \Kaeru\Storage\Db\Query\SelectQuery $query
     * @return \Kaeru\Model\AbstractModel
     */
    protected function queryAndCreateModel(SelectQuery $query)
    {
        if (isset($this->modelCache[$this->currentModelId])) {
            return $this->modelCache[$this->currentModelId];
        }
        
        $cacheKey = $this->createCacheKey($query->getSQL(), $query->getValues());
        if ($this->cacheStorage !== null
            && $this->cacheStorage->test($cacheKey)) {
                return $this->cacheStorage->load($cacheKey);
        }

        $model = null;
        $result = $this->storage->query($query);
        if ($result) {
            $primaryKey = $this->primaryKey;
            $model = $this->create();

            // if ($result->getAffectedRows() > 1) {
            // 	WARNING: expected 1 row, got $result->getAffectedRows()
            // }
            // $row = array_shift($result->toArray());
            $row = array_shift($result);
            $this->setModelProperties($model, $row)
                 ->applyPropertyProxies($model);

            $this->modelCache[$model->$primaryKey] = $model;

            if ($this->cacheStorage !== null) {
                $this->cacheStorage->save($cacheKey, $model);
                $this->mapCacheKey($cacheKey, $model->$primaryKey);
            }
        }
        $this->currentModelId = null;
        return $model;
    }

    /**
     * Queries the database, initializes models and sets
     * their properties values from the result (if any).
     *
     * @todo Borde cache:a alla objekt för sig själv och sedan referera till dem
     *       istället... 
     * @param \Kaeru\Storage\Db\Query\SelectQuery $query
     * @return array
     */
    protected function queryAndCreateModels(SelectQuery $query)
    {
        $cacheKey = $this->createCacheKey($query->getSQL(), $query->getValues());
        if ($this->cacheStorage !== null
            && $this->cacheStorage->test($cacheKey)) {
                return $this->cacheStorage->load($cacheKey);
        }

        $resultArray = null;
        $result = $this->storage->query($query);
        if ($result) {
            $resultArray = array();
            foreach ($result as $row) {
                $primaryKey = $this->primaryKey;
                if (isset($this->modelCache[$row[$primaryKey]])) {
                    $model = $this->modelCache[$row[$primaryKey]];
                } else {
                    $model = $this->create();
                    $this->setModelProperties($model, $row)
                         ->applyPropertyProxies($model);

                    $this->modelCache[$model->$primaryKey] = $model;
                }
                $resultArray[$model->$primaryKey] = $model;
            }
            if ($this->cacheStorage !== null) {
                $this->cacheStorage->save($cacheKey, $resultArray);
            }
        }
        return $resultArray;
    }

    /**
     * Updates any cache of the model.
     *
     * @param \Kaeru\Model\AbstractModel $model
     * @return void
     */
    protected function updateCache($model)
    {
        $primaryKey = $this->primaryKey;
        if ($this->cacheStorage !== null) {
            if (!isset($this->cacheKeyMapping[$model->$primaryKey])) {
                return;
            }
            foreach ($this->cacheKeyMapping[$model->$primaryKey] as $cacheKey) {
                $this->cacheStorage->save($cacheKey, $model);
            }
        }
    }
    
    /**
     * Saves the model data to the database. If the primary key property on the
     * model is null, a new row will be inserted in the database. Else
     * an update will be made.
     *
     * @param Kaeru\Model\AbstractModel $model
     * @return Kaeru\Model\AbstractModel
     */
    public function save(AbstractModel $model, $force = null)
    {
        $primaryKey = $this->primaryKey;
        // TODO: Is this necessary?
        $this->validate($model);
        if (!$model->isValid() && !$force) {
            return false;
        }

        $dataTemplate = array_fill_keys($this->modelProperties, null);

        $modelData = $model->getProperties();
        $sanitizedData = Utility::arrayOverlay($dataTemplate, $modelData);

        // Property to column mapping
        foreach ($sanitizedData as $key => $value) {
            if (array_key_exists($key, $this->tableColumnMapping)) {
                $column = $this->tableColumnMapping[$key];
                $sanitizedData[$column] = $sanitizedData[$key];
                unset($sanitizedData[$key]);
            }
        }
        unset($sanitizedData[$primaryKey]);

        if (!empty($model->$primaryKey)) {
            $pKey = $model->$primaryKey;
            $this->storage->update(
                $this->tableName,
                $sanitizedData,
                $this->primaryKey . ' =',
                $pKey
            );
        } else {
            $lastInsertId = $this->storage->insert($this->tableName, $sanitizedData);
            if ($lastInsertId === false) {
                // FIXME:
                throw new \Exception('Could not INSERT...');
            }
            $model->$primaryKey = $lastInsertId;
        }

        $this->updateCache($model);

        return $model;
    }

    /**
     *
     *
     * @param \Kaeru\Model\AbstractModel $model
     * @param array $data
     * @return \Kaeru\Model\Dao\AbstractDbDao
     */
    protected function setModelProperties(AbstractModel $model, array $data)
    {
        $modelProperties = array();
        foreach ($data as $column => $value) {
            $property = array_search($column, $this->tableColumnMapping);
            if (!$property) {
                $property = $column;
            }
            $modelProperties[$property] = $value;
        }

        $model->setProperties($modelProperties);
        return $this;
    }

    /**
     * Sets a specific primary key column (overrides the default 'id').
     *
     * @param $primaryKey
     * @return \Kaeru\Model\Dao\AbstractDbDao
     */
    public function setPrimaryKey($primaryKey)
    {
        $this->primaryKey = $primaryKey;
        return $this;
    }

    /**
     * Sets the database table to fetch from.
     *
     * @return \Kaeru\Model\Dao\AbstractDbDao
     * @param string $tableName
     */
    public function setTableName($tableName)
    {
        $this->tableName = $tableName;
        return $this;
    }
}
